﻿// -------------------- iSharePoint -------------------- //
// 
//  Company:	IT Complex, LLC
//  Project:	Smart.Common
//  File name:	SPSecurityExts.cs
//  Developer:	Igor Solomatov
//  Created:	03.03.2013
// 
// -------------------- iSharePoint -------------------- //

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Microsoft.SharePoint;

namespace Smart.Common
{
	[Localizable(false)]
	public static class SPSecurityExts
	{
		/// <summary>
		/// Checks whether <paramref name="one"/> role definition is equals to <paramref name="another"/>
		/// </summary>
		public static bool Eq(this SPRoleDefinition one, SPRoleDefinition another)
		{
			return
				ReferenceEquals(one, another) || (one != null && another != null && one.Id == another.Id);
		}

		/// <summary>
		/// Checks whether <paramref name="one"/> role definition is in <paramref name="others"/> collection
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="others"/> is null</exception>
		public static bool In(this SPRoleDefinition one, params SPRoleDefinition[] others)
		{
			if (others == null) throw new ArgumentNullException("others");

			if (one == null)
				return false;

			for (int i = 0, iMax = others.Length; i < iMax; i++)
			{
				var another = others[i];

				if (ReferenceEquals(one, another) || (one.Id == another.Id))
					return true;
			}

			return false;
		}

		public const string SharePointSystemLogin = @"SHAREPOINT\system";

		/// <summary>
		/// Ensures assignment <paramref name="definition"/> exists for <paramref name="principal"/>.
		/// If <paramref name="restrict"/> specified removes others definitions, except <paramref name="limited"/> access.
		/// If <paramref name="currentScopeOnly"/> specified, removes only from current scope.
		/// </summary>
		public static void EnsureAssignment(this SPRoleAssignmentCollection roleAssignments, SPPrincipal principal, SPRoleDefinition definition, SPRoleDefinition limited, bool currentScopeOnly, bool restrict, ref bool changed)
		{
			// ReSharper disable LoopCanBeConvertedToQuery

			SPRoleAssignment assignment;

			try
			{
				assignment = roleAssignments.GetAssignmentByPrincipal(principal);
			}
			catch
			{
				assignment = null;
			}

			if (assignment != null)
			{
				var exists = false;

				var assignmentDefinitions = assignment.RoleDefinitionBindings;

				foreach (SPRoleDefinition assignmentDefinition in assignmentDefinitions)
				{
					if (assignmentDefinition.Eq(definition))
					{
						exists = true;
						break;
					}
				}

				var assignmentChanged = false;

				if (!exists)
				{
					assignmentDefinitions.Add(definition);
					assignmentChanged = true;
				}

				if (restrict)
				{
					var definitionsToRemove = new Stack<int>();

					for (int i = 0, iMax = assignmentDefinitions.Count; i < iMax; i++)
					{
						var assignmentDefinition = assignmentDefinitions[i];

						if (!assignmentDefinition.In(definition, limited))
							definitionsToRemove.Push(i);
					}

					while (definitionsToRemove.Count > 0)
					{
						assignmentDefinitions.Remove(definitionsToRemove.Pop());
						assignmentChanged = true;
					}
				}

				if (assignmentChanged)
				{
					assignment.Update();
					changed = true;
				}
			}
			else
			{
				assignment = new SPRoleAssignment(principal);

				assignment.RoleDefinitionBindings.Add(definition);

				if (currentScopeOnly)
					roleAssignments.AddToCurrentScopeOnly(assignment);
				else
					roleAssignments.Add(assignment);

				changed = true;
			}

			// ReSharper restore LoopCanBeConvertedToQuery
		}

		/// <summary>
		/// Ensures <paramref name="item"/> security assignments has all specified <paramref name="assignments"/>, loads definitions from <paramref name="item"/>'s Web.
		/// If <paramref name="restrict"/> specified, removes others definitions, except <paramref name="limited"/> access, and other principals except system.
		/// If <paramref name="currentScopeOnly"/> specified, removes only from current scope.
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="item"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="assignments"/> is null</exception>
		public static bool EnsureRoleAssignments(this SPListItem item, IEnumerable<Cortege<SPPrincipal, string>> assignments, bool currentScopeOnly, bool restrict)
		{
			return EnsureRoleAssignments(item, item.Web, assignments, currentScopeOnly, restrict);
		}

		/// <summary>
		/// Ensures <paramref name="item"/> security assignments has all specified <paramref name="assignments"/>, loads definitions from <paramref name="web"/>.
		/// If <paramref name="restrict"/> specified, removes others definitions, except <paramref name="limited"/> access, and other principals except system.
		/// If <paramref name="currentScopeOnly"/> specified, removes only from current scope.
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="web"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="item"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="assignments"/> is null</exception>
		public static bool EnsureRoleAssignments(this SPSecurableObject item, SPWeb web, IEnumerable<Cortege<SPPrincipal, string>> assignments, bool currentScopeOnly, bool restrict)
		{
			if (web == null) throw new ArgumentNullException("web");

			var definitions = web.RoleDefinitions;
			var roleDefinitions = new Dictionary<string, SPRoleDefinition>(StringComparer.OrdinalIgnoreCase); // local caching

			return
				EnsureRoleAssignments(
					item,
					assignments
						.Select(
							assignment =>
							new Cortege<SPPrincipal, SPRoleDefinition>(
								false,
								assignment.Arg1,
								roleDefinitions.SafeGetLoad(assignment.Arg2, name => definitions[assignment.Arg2]))),
					definitions.GetLimitedAccessDefinition(),
					currentScopeOnly,
					restrict);
		}

		/// <summary>
		/// Ensures <paramref name="item"/> security assignments has all specified <paramref name="assignments"/>, loads limited definition from <paramref name="item"/>'s Web.
		/// If <paramref name="restrict"/> specified, removes others definitions, except <paramref name="limited"/> access, and other principals except system.
		/// If <paramref name="currentScopeOnly"/> specified, removes only from current scope.
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="item"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="assignments"/> is null</exception>
		/// <exception cref="ArgumentException">Argument <paramref name="assignments"/> has invalid entries with empty values!</exception>
		public static bool EnsureRoleAssignments(this SPListItem item, IEnumerable<Cortege<SPPrincipal, SPRoleDefinition>> assignments, bool currentScopeOnly, bool restrict)
		{
			return EnsureRoleAssignments(item, assignments, item.Web.RoleDefinitions.GetLimitedAccessDefinition(), currentScopeOnly, restrict);
		}

		/// <summary>
		/// Ensures <paramref name="item"/> security assignments has all specified assignments, loads limited definition from <paramref name="item"/>'s Web.
		/// If <paramref name="restrict"/> specified, removes others definitions, except limited access, and other principals except system.
		/// If <paramref name="currentScopeOnly"/> specified, removes only from current scope.
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="item"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="fullAccessDefinition"/> is null and <paramref name="fullAccessMembers"/> was specified</exception>
		/// <exception cref="ArgumentNullException"><paramref name="writeDefinition"/> is null and <paramref name="writeMembers"/> was specified</exception>
		/// <exception cref="ArgumentNullException"><paramref name="readDefinition"/> is null and <paramref name="readMembers"/> was specified</exception>
		/// <exception cref="ArgumentException">Argument assignments has invalid entries with empty values</exception>
		public static bool EnsureRoleAssignments(this SPListItem item, List<SPPrincipal> fullAccessMembers, SPRoleDefinition fullAccessDefinition, List<SPPrincipal> writeMembers, SPRoleDefinition writeDefinition, List<SPPrincipal> readMembers, SPRoleDefinition readDefinition, bool currentScopeOnly, bool restrict)
		{
			var allAssignments = new List<Cortege<SPPrincipal, SPRoleDefinition>>();

			// ReSharper disable LoopCanBeConvertedToQuery
			if (fullAccessMembers != null)
			{
				if (fullAccessDefinition == null) throw new ArgumentNullException("fullAccessDefinition");

				foreach (var member in fullAccessMembers)
				{
					if (member != null)
						allAssignments.Add(new Cortege<SPPrincipal, SPRoleDefinition>(member, fullAccessDefinition));
				}
			}

			if (writeMembers != null)
			{
				if (writeDefinition == null) throw new ArgumentNullException("writeDefinition");

				foreach (var member in writeMembers)
				{
					if (member != null)
						allAssignments.Add(new Cortege<SPPrincipal, SPRoleDefinition>(member, writeDefinition));
				}
			}

			if (readMembers != null)
			{
				if (readDefinition == null) throw new ArgumentNullException("readDefinition");

				foreach (var member in readMembers)
				{
					if (member != null)
						allAssignments.Add(new Cortege<SPPrincipal, SPRoleDefinition>(member, readDefinition));
				}
			}
			// ReSharper restore LoopCanBeConvertedToQuery

			return item.EnsureRoleAssignments(allAssignments, currentScopeOnly, restrict);
		}

		/// <summary>
		/// Ensures <paramref name="item"/> security assignments has all specified <paramref name="assignments"/>.
		/// If <paramref name="restrict"/> specified, removes others definitions, except <paramref name="limited"/> access, and other principals except system.
		/// If <paramref name="currentScopeOnly"/> specified, removes only from current scope.
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="item"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="assignments"/> is null</exception>
		/// <exception cref="ArgumentException">Argument <paramref name="assignments"/> has invalid entries with empty values</exception>
		public static bool EnsureRoleAssignments(this SPSecurableObject item, IEnumerable<Cortege<SPPrincipal, SPRoleDefinition>> assignments, SPRoleDefinition limited, bool currentScopeOnly, bool restrict)
		{
			if (item == null) throw new ArgumentNullException("item");
			if (assignments == null) throw new ArgumentNullException("assignments");

			var changed = false;

			if (!item.HasUniqueRoleAssignments)
			{
				changed = true;

				item.BreakRoleInheritance(false);
			}

			var existingPrincipals = new HashSet<int>();
			var principalsAssignments = new DictionaryList<int, SPPrincipal, SPRoleDefinition>();

			foreach (var assignment in assignments)
			{
				var principal = assignment.Arg1;
				var definition = assignment.Arg2;

				if (principal == null || definition == null)
					throw new ArgumentException("Argument assignments has invalid entries with empty values!", "assignments");

				principalsAssignments.SafeAdd(principal.ID, principal, definition);
			}

			var assignmentsToRemove = new Stack<SPPrincipal>();

			var existingAssignments = item.RoleAssignments;

			for (int assignmentIdx = 0, assignmentIdxMax = existingAssignments.Count; assignmentIdx < assignmentIdxMax; assignmentIdx++)
			{
				var existingAssignment = existingAssignments[assignmentIdx];

				var principal = existingAssignment.Member;

				var newDefinitions = principalsAssignments.SafeGetList(principal.ID);

				if (newDefinitions != null)
				{
					// Ensure existing assignment
					existingPrincipals.Add(principal.ID);

					var definitionsToRemove = new Stack<int>();
					var definitionsToAdd = new Dictionary<int, SPRoleDefinition>();

					foreach (var newDefinition in newDefinitions)
						if (newDefinition.Type != SPRoleType.Guest)
							definitionsToAdd[newDefinition.Id] = newDefinition;

					var existingDefinitions = existingAssignment.RoleDefinitionBindings;

					for (int i = 0, iMax = existingDefinitions.Count; i < iMax; i++)
					{
						var existingDefinition = existingDefinitions[i];

						if (definitionsToAdd.ContainsKey(existingDefinition.Id))
						{
							definitionsToAdd.Remove(existingDefinition.Id);
						}
						else if (restrict && !existingDefinition.Eq(limited))
						{
							definitionsToRemove.Push(i);
						}
					}

					var assignmentChanged = false;

					// Remove first, as indexes can be corrupted

					while (definitionsToRemove.Count > 0)
					{
						var definitionIdx = definitionsToRemove.Pop();

						existingDefinitions.Remove(definitionIdx);
						assignmentChanged = true;
					}

					// Add next after deletion, as indexes can be corrupted

					foreach (var kvp in definitionsToAdd)
					{
						existingDefinitions.Add(kvp.Value);
						assignmentChanged = true;
					}

					if (assignmentChanged)
					{
						existingAssignment.Update();
						changed = true;
					}
				}
				else if (restrict && !principal.LoginName.EqIC(SharePointSystemLogin))
				{
					// Check for "Limited" rights to remove or cleanup

					var assignmentDefinitions = existingAssignment.RoleDefinitionBindings;

					var limitedIdx = -1;

					for (int i = 0, iMax = assignmentDefinitions.Count; i < iMax; i++)
					{
						var roleDefinition = assignmentDefinitions[i];

						if (roleDefinition.Eq(limited))
						{
							limitedIdx = i;
							break;
						}
					}

					if (limitedIdx < 0)
					{
						// Completely remove assignment
						assignmentsToRemove.Push(principal);
					}
					else
					{
						var assignmentChanged = false;

						// Cleanup assignment
						for (var i = assignmentDefinitions.Count - 1; i > limitedIdx; i--)
						{
							assignmentDefinitions.Remove(i);
							assignmentChanged = true;
						}

						for (var i = limitedIdx - 1; i >= 0; i--)
						{
							assignmentDefinitions.Remove(i);
							assignmentChanged = true;
						}

						if (assignmentChanged)
						{
							existingAssignment.Update();
							changed = true;
						}
					}
				}
			}

			// Remove first, to minimize overhead

			while (assignmentsToRemove.Count > 0)
			{
				var principalToRemove = assignmentsToRemove.Pop();

				if (currentScopeOnly)
					existingAssignments.RemoveFromCurrentScopeOnly(principalToRemove);
				else
					existingAssignments.Remove(principalToRemove);

				changed = true;
			}

			// After removal add new assignments

			foreach (var kvp in principalsAssignments)
			{
				if (existingPrincipals.Contains(kvp.Key)) continue;

				var newAssignment = new SPRoleAssignment(kvp.Value.Arg1);
				var newAssignmentDefinitions = newAssignment.RoleDefinitionBindings;

				var duplicateDefinitionsChecker = new HashSet<int>();

				foreach (var newDefinition in kvp.Value.Arg2)
					if (duplicateDefinitionsChecker.Add(newDefinition.Id) && newDefinition.Type != SPRoleType.Guest)
						newAssignmentDefinitions.Add(newDefinition);

				if (currentScopeOnly)
					existingAssignments.AddToCurrentScopeOnly(newAssignment);
				else
					existingAssignments.Add(newAssignment);

				changed = true;
			}

			return changed;
		}

		/// <summary>
		/// Copy roles assignments from source principal to destination principal. Item is not updated (update is not required).
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="item"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="web"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="srcPrincipal"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="dstPrincipal"/> is null</exception>
		public static bool CopyRoleAssignments(this SPListItem item, SPPrincipal srcPrincipal, SPPrincipal dstPrincipal, bool currentScopeOnly)
		{
			return CopyRoleAssignments(item, item.Web, srcPrincipal, dstPrincipal, currentScopeOnly);
		}

		/// <summary>
		/// Copy roles assignments from source principal to destination principal. Item is not updated (update is not required).
		/// </summary>
		/// <exception cref="ArgumentNullException"><paramref name="item"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="web"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="srcPrincipal"/> is null</exception>
		/// <exception cref="ArgumentNullException"><paramref name="dstPrincipal"/> is null</exception>
		public static bool CopyRoleAssignments(this SPSecurableObject item, SPWeb web, SPPrincipal srcPrincipal, SPPrincipal dstPrincipal, bool currentScopeOnly)
		{
			if (item == null) throw new ArgumentNullException("item");
			if (web == null) throw new ArgumentNullException("web");
			if (srcPrincipal == null) throw new ArgumentNullException("srcPrincipal");
			if (dstPrincipal == null) throw new ArgumentNullException("dstPrincipal");

			// Nothing to copy
			if (!item.HasUniqueRoleAssignments)
				return true;

			var roleAssignments = item.RoleAssignments;

			SPRoleAssignment sourceAssignment = null;

			try
			{
				sourceAssignment = roleAssignments.GetAssignmentByPrincipal(srcPrincipal);
			}
			catch (Exception x)
			{
				H.LX("CopyRoleAssignments.Source", x);
			}

			// Nothing to copy
			if (sourceAssignment == null)
				return true;

			try
			{
				try
				{
					if (dstPrincipal is SPUser)
						dstPrincipal = web.SafeEnsureUser(dstPrincipal.LoginName);
				}
				catch (Exception x)
				{
					H.LX("CopyRoleAssignments.EnsureUser", x);
				}

				SPRoleAssignment destinationAssignment = null;

				try
				{
					destinationAssignment = roleAssignments.GetAssignmentByPrincipal(dstPrincipal);
				}
				catch (Exception x)
				{
					H.LX("CopyRoleAssignments.Destination", x);
				}

				var destinationCreated = destinationAssignment == null;
				var destinationChanged = false;

				if (destinationCreated)
					destinationAssignment = new SPRoleAssignment(dstPrincipal);

				// My girl is the little Naughty cat!

				var newDefinitions = new Dictionary<int, SPRoleDefinition>();

				foreach (SPRoleDefinition roleDefinition in sourceAssignment.RoleDefinitionBindings)
					if (roleDefinition.Type != SPRoleType.Guest)
						newDefinitions[roleDefinition.Id] = roleDefinition;

				var definitions = destinationAssignment.RoleDefinitionBindings;

				foreach (SPRoleDefinition existingDefinition in definitions)
					newDefinitions.Remove(existingDefinition.Id);

				foreach (var newDefinition in newDefinitions.Values)
				{
					destinationChanged = true;
					definitions.Add(newDefinition);
				}

				if (destinationCreated)
				{
					if (currentScopeOnly)
						roleAssignments.AddToCurrentScopeOnly(destinationAssignment);
					else
						roleAssignments.Add(destinationAssignment);
				}
				else if (destinationChanged)
				{
					destinationAssignment.Update();
				}
			}
			catch (Exception x)
			{
				H.LX("CopyRoleAssignments", x);
				return false;
			}

			return true;
		}

		/// <summary>
		/// Resets security role assignments
		/// </summary>
		public static void ResetSecurityAssignmentsElevated(SPListItem item)
		{
			if (!item.HasUniqueRoleAssignments)
				return;

			item.Web.RunElevated(
				delegate
				{
					try
					{
						var eSite = SPH.GetElevatedSite();
						var eWeb = SPH.GetElevatedWeb();

						using (new UnsafeUpdate(eSite, eWeb))
						{
							var eList = eWeb.Lists[item.ParentList.ID];
							var eItem = eList.GetItemById(item.ID);

							if (eItem.HasUniqueRoleAssignments)
							{
								eItem.ResetRoleInheritance();
								//eItem.Update();
							}
						}
					}
					catch (Exception x)
					{
						H.LX("ResetSecurityAssignments", x);
					}
				});
		}

		/// <summary>
		/// Returns limited access role definition
		/// </summary>
		/// <param name="roleDefinitions">Role definition collection</param>
		public static SPRoleDefinition GetLimitedAccessDefinition(this SPRoleDefinitionCollection roleDefinitions)
		{
			return roleDefinitions.GetByType(SPRoleType.Guest);
		}

		private static readonly char[] LoginSeparators = new[] { '\\', '@' };

		/// <summary>
		/// Checks whether string is user login
		/// </summary>
		public static bool IsUsersLogin(this string loginName)
		{
			return (!loginName.IsNullOrEmpty()) && (loginName.IndexOfAny(LoginSeparators) > 0);
		}

		/// <summary>
		/// Ensures login is user login, if not throws exception
		/// </summary>
		/// <exception cref="SPException">User could not be found</exception>
		public static void EnsureUserLogin(this string loginName)
		{
			if (!IsUsersLogin(loginName))
				throw new SPException(SPResource.GetString("UserCouldNotBeFound", loginName));
		}

		/// <summary>
		/// Checks whether <paramref name="securable"/> has unique assignments
		/// </summary>
		public static bool SafeHasUniqueRoleAssignments(this SPSecurableObject securable)
		{
			if (securable == null)
				return false;

			try
			{
				return securable.HasUniqueRoleAssignments;
			}
			catch (Exception x)
			{
				//H.LX("SPListItemExts.SafeHasUniqueRoleAssignments", x);
				H.LT("SPSecurityExts.SafeHasUniqueRoleAssignments: Can't check HasUniqueRoleAssignments, " + x.Message);
				return false;
			}
		}
	}
}